from flask.signals import Namespace
import sqlalchemy
from flask import Flask, session
from flask_sqlalchemy import SQLAlchemy
from flask_login import current_user
from flask_principal import Principal, identity_changed, Identity, Permission
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

signals = Namespace()

permission_changed = signals.signal('permission_changed', doc='''
a signal emit by user to notify internal to invoke permission reload
''')

loading_addition_roles = signals.signal('loading_addition_roles', doc='''
@loading_addition_roles.connect_via(app)
def on_loading_addition_roles(sender, roles):
    roles.add('project_namanger')
    roles.add('employee_manager')
''')

loading_addition_actions = signals.signal('loading_addition_actions', doc='''
@loading_addition_actions.connect_via(app)
def on_loading_addition_actions(sender, actions):
    actions.add('project_edit_')
''')

loading_action_values = signals.signal('loading_action_values', doc='''
@loading_action_values.connect_var(app)
def on_loading_action_values(sender, action, values):
    if action == 'project_edit':
        values.add('A_PROJECT_NUM')
''')

Base = declarative_base()

class RoleModel(Base):
    __tablename__ = 'mixed_ac_role'

    id = sqlalchemy.Column(sqlalchemy.Integer, primary_key=True)
    role_name = sqlalchemy.Column(sqlalchemy.String(50), unique=True)
    actions = sqlalchemy.Column(sqlalchemy.String(1000))


class UserPermissionModel(Base):
    __tablename__ = 'mixed_ac_user_permission'

    id = sqlalchemy.Column(sqlalchemy.Integer, primary_key=True)
    user_id = sqlalchemy.Column(sqlalchemy.String(40), unique=True)
    roles = sqlalchemy.Column(sqlalchemy.String(1000))
    actions = sqlalchemy.Column(sqlalchemy.String(1000))


class MixedAccessControl(Principal):
    '''Mixed access control state class

    example:
        mixed_ac = MixedAccessControl(app)
    '''
    testing_mode = False
    testing_user_id = ''

    def __init__(self, app:Flask=None, db:SQLAlchemy=None, use_sessions=True, skip_static=False):
        assert app is not None
        assert db is not None

        self.app = app
        self.db = db
        self.actions = set()
        self.permissions = {}
        self.related_actions = {}

        super(MixedAccessControl, self).__init__(app, use_sessions, skip_static)

        Base.metadata.create_all(db.engine)

        identity_changed.connect(self._on_identity_changed2, app)

    # begin database operation helpers

    def insert_role(self, role_name, actions=[]):
        self.db.session.add(
            RoleModel(role_name=role_name, actions=','.join(actions)))

    def select_role_actions(self, role_name):
        r = self.db.session.query(RoleModel)\
            .filter_by(role_name=role_name).first()
        if r:
            return r.actions.split(',')
        return []

    def update_role_actions(self, role_name, actions):
        self.db.session.update(
            RoleModel).filter_by(role_name=role_name).values(actions=','.join(actions))

    def insert_user_permission(self, user_id, roles=[], actions=[]):
        self.db.session.add(
            UserPermissionModel(user_id=user_id, roles=','.join(roles), actions=','.join(actions)))

    def select_user_actions(self, user_id):
        r = self.db.session.query(UserPermissionModel)\
            .filter_by(user_id=user_id).first()
        if r:
            roles = r.roles.split(',')
            actions = r.actions.split(',')
            for role_name in roles:
                actions += self.select_role_actions(role_name)
            return actions
        return []

    def update_user_permission(self, user_id, roles=[], actions=[]):
        self.db.session.query(UserPermissionModel)\
            .filter_by(user_id=user_id)\
            .update({'roles': ','.join(roles), 'actions': ','.join(actions)})

    # end database operation helpers

    def _on_identity_changed2(self, app, identity: Identity):
        user_id = self.testing_user_id

        if not self.testing_mode:
            assert hasattr(current_user, 'id')
            user_id = current_user.id

        actions = set()

        actions |= set(self.select_user_actions(user_id))

        roles = set()

        # collect addition roles
        loading_addition_roles.send(self.app, roles=roles)

        for role_name in roles:
            actions |= set(self.select_role_actions(role_name))

        loading_addition_actions.send(self.app, actions=actions)

        for action in actions:
            identity.provides |= self._make_needs(action)

        session['permited_actions'] = identity.provides

    def register_action(self, actions):
        '''register a general action that not bind to any data
        :param actions:
        :return:
        '''
        assert isinstance(actions, list) or isinstance(actions, set)
        for action in actions:
            self.actions.add(action)

    def register_value_action(self, actions, related_action):
        '''a value action always has a related action, the related action
        is usually a general action like project_edit and not bind to any data
        :param actions:
        :param related_action:
        :return:
        '''
        assert isinstance(actions, list) or isinstance(actions, set)
        for action in actions:
            self.related_actions[action] = related_action

    def make_permission(self, *needs):
        class ActionPermission(Permission):
            def __init__(self, parent: MixedAccessControl, *needs):
                _needs = set()
                permited_actions = set()
                if 'permited_actions' in session:
                    permited_actions = session['permited_actions']
                for need in needs:
                    action = need[0]
                    handled = False
                    if action in parent.related_actions:
                        related_action = parent.related_actions[action]
                        # only add permited actions that related to this action
                        if (related_action, ) in permited_actions:
                            handled = True
                            _needs.add((related_action, ))

                    if not handled:
                        _needs.add(need)
                super(ActionPermission, self).__init__(self, *_needs)

        return ActionPermission(self, *needs)

    def _make_needs(self, action):
        needs = set()
        if action in self.actions:
            needs.add((action, ))
        elif action in self.related_actions:
            values = set()
            if action in session:
                values = session[action]
            else:
                # collection action values
                loading_action_values.send(self.app, action=action, values=values)
                session[action] = values
                session.modified = True
            for value in values:
                needs.add((action, value))
        else:
            assert False
        return needs